Conversation
|
|
||
| The action of a class definition statement is to allocate the class object, define its functions and properties, and freeze it. Consequently, a class cannot be instantiated before this statement is executed. | ||
|
|
||
| We do, however, *hoist* the class identifier's binding to the top of the script so that it can be referred to within functions or classes that lexically appear before the class definition. This makes it easy and straightforward for developers to write classes or functions that mutually refer to one another. |
There was a problem hiding this comment.
Class hoisting implies the following code is valid:
local function getFoo()
local f = Multiple.new(2)
return f
end
print(getFoo()) -- ???
local function getData(factor: number)
return 123 * factor
end
class Multiple
public factor: number
function length(self)
return getData(factor)
end
endWhat is the expected behavior where getFoo() is called? A runtime error?
There was a problem hiding this comment.
Yeah. It's a runtime error. Multiple will have the value nil if getFoo is invoked before the class statement is evaluated. The RFC describes this a little bit further into the document.
There was a problem hiding this comment.
I am completely against introducing any hoisting to the language, with this case being a perfect example of why.
If someone needs to reference a class before it is defined, they should use a forward-declared variable as a surrogate to explicitly show that they are using a token that may or may not be defined yet.
There was a problem hiding this comment.
Also, the presence of hoisting seems to be part of the reason why class definitions aren't allowed outside the top scope, since hoisting scoped variables can get messy. I would wager everyone would prefer to have scoped classes over class hoisting if they had to choose between the two.
There was a problem hiding this comment.
Without hoisting, there is no way at all to write two classes that mutually refer to one another.
The top-level constraint is about establishing that there is exactly on instance of every method of every class. This makes it significantly easier for us to do static method dispatch.
I'll update the RFC to clarify this. Thanks!
There was a problem hiding this comment.
There's nothing wrong here. Classes as a statement has zero code to execute outside of the initialization. Fields do not have an initializer. Just don't let functions or locals reference any classes that are defined later.
local function foo()
return Foo.new()
-- ^^^ `Foo` is not bound in scope here
end
class Foo -- now `Foo` is bound in scope
bar: Bar
function new()
return Foo { bar = Bar { x = 5 } }
end
end
print(Foo.new().bar.x) -- prints 5. It's fine.
class Bar -- now `Bar` is bound in scope
x: number
end
-- ...the rest of the module...The right way to do the hoisting is by doing something like this:
class <unutterable-1>
bar: <unutterable-2>
function new()
return <unutterable-1> { bar = <unutterable-2> { x = 5 } }
end
end
class <unutterable-2>
x: number
end
local function foo()
return Foo.new()
-- ^^^ `Foo` is not bound in scope here
end
const Foo = <unutterable-1>
print(Foo.new().bar.x) -- prints 5. It's fine.
const Bar = <unutterable-2>That is, what you do is group all classes into a mutually dependent block that can reference each other unconditionally, and then and only then do you incrementally bring the name of the individual class into scope.
The reason why hoisting in JS gets so much hate is because it relies on TDZ which cannot be analyzed at compile time, that's literally why TDZ exists at runtime. Here, we can warn when attempting to reference Foo that is not yet bound in scope. You have a warning, so this isn't hoisting in the same sense as JS hoisting.
There was a problem hiding this comment.
That said, the RFC as is does phrase it like the solution is either-or: 1. no way for two classes to be defined out-of-order or mutually dependent in some way, or 2. do TDZ and say all class identifiers are hoisted to the top.
The solution I suggested avoids this problem, as long as classes do not have any initializers.
There was a problem hiding this comment.
Sigh. Flip the direction. Functions in class definitions can capture locals. TDZ.
class Bar
function run_foo()
Foo.print_x()
end
end
Bar.run_foo()
local x = 5
class Foo
function print_x()
print(x)
end
endThere was a problem hiding this comment.
What you're describing is an explicit phrasing of exactly what the RFC proposes except that we lose the constness of the class object. Prior to the definition of ClassB, its name is in scope but has the value nil.
It's not the best but I don't see a viable alternative.
I also don't know if it's really all that bad: It only arises when code is interleaving class definitions and executing imperative actions at the module scope.
There was a problem hiding this comment.
...So class variables are effectively globals?
If that's the case, how do shadowing and global variables behave with class definitions?
local A = 1 -- shadows A?
print(A) -- 1 or nil?
class A end
print(A) -- class A?
B = 2 -- global variable or parse error from reassignment?
print(B) -- 2 or nil?
class B end
print(getfenv()["B"]) -- 2?Or what if the module returns early before a class is initialized?
class A
function f()
return B {} -- 50/50 chance always nil?
end
end
if math.random(0, 1) == 1 then return end
class B endEven if a set of consistent rules can be made, it just seems like a lot of footguns are going to be caused by this.
|
|
||
| Reading or writing a nonexistent class property raises an exception. This makes it easy to disambiguate between a nonexistent property and a property whose value is nil. | ||
|
|
||
| The builtin `type()` and `typeof()` functions return `"object"` for any class instance. We chose this over having them return the class name because class names do not have to be globally unique (they must only unique within a single module) and because we do not want to make it possible for classes to impersonate other types. |
There was a problem hiding this comment.
I've always interpreted type to be the safe one that tells me the true type and typeof to be the one that might lie to me about the actual type. Any reason we wouldn't also do that here? Alternatively, what if typeof returned as a second argument, the pointer to the class?
There was a problem hiding this comment.
The main problem I want to really address is that type and typeof both work with strings.
Class names aren't required to be globally unique, so you could get wedged into a bad situation if a class happens to be unfortunately named.
instanceof is designed to solve this problem by working with class objects directly.
There was a problem hiding this comment.
I'd like to avoid code where I am mixing typeof, instanceof, and getmetatable (for table-based classes). Maybe it's not actually that bad in practice - but it would be nice to have a single method that tells me about somethings type.
Additionally, it can be nice to use these methods to 'inspect' the type. With instanceof I must enumerate all possible types the object can be. Sometimes I might want to write:
local supportedTypes = {
[FooClass] = true,
[BarClass] = true,
[BazClass] = false,
}
-- Version I'd like to write
function isSupportedType(object)
local T = typeof(object) -- May not be the exact statement you'd write
return supportedTypes[T]
end
-- Version I'd need to write with current proposal
function isSupportedType(object)
for T, supported in supportedTypes do
if instanceof(object, T) then
return supported
end
end
end
There was a problem hiding this comment.
I've always interpreted
typeto be the safe one that tells me the true type andtypeofto be the one that might lie to me about the actual type.
This is incorrect, neither type nor typeof can lie about the type name for performance and embedder sandboxing.
There was a problem hiding this comment.
Also, let's be honest. I don't think the code in the example is particularly great. Just write the return instanceof(o, FooClass) or instanceof(o, BarClass) or .... Dumb code is good code.
|
|
||
| ```luau | ||
| class Point | ||
| public x: number |
There was a problem hiding this comment.
Is there a way for me to define a static member such as Point.zero?
There was a problem hiding this comment.
Not yet. It's a good idea for an extension though.
|
The existence of a new VM type raises a question for the type checker. Obviously, I imagine the use would mostly be for inverting type requirements to accept anything that wasn't an object (for e.g. serializing data) which might be niche but it's still worth considering in my opinion. |
Why limit Local, function, and type bindings can be defined in any scope, therefore it'd make sense to allow the same for classes. |
|
Certainly an interesting RFC so far. A couple of questions: Do we need access specifiers in Luau? Majority of the code written today do not need private fields, and since classes can capture up-values, do we truly need private fields? And since private fields are not going to be supported out of the box, it seems a little weird to include the Why create a whole new type in the VM? Couldn't a syntax sugar be implemented in place for table objects with a metatable instead? It seems unnecessary to me. Also, I believe inheritation will be important, as at the moment, you can already create classes with a lot of functionality and decent support for the type-checker (except for shared-self types, which should be coming eventually!), but when it comes to inheritation, things get rather difficult. |
|
@deviaze would you be able to give an example of this?
|
Sure! Here, the |
|
This doesn't seem to be stated in the RFC, but if |
|
What does the C API look like for classes? |
| ```luau | ||
| class Point | ||
| public x: number | ||
| public y |
There was a problem hiding this comment.
Can class fields have default values?
There was a problem hiding this comment.
Not yet. Sounds like a nice extension though.
|
Just had a quick skim, but I noticed |
|
|
||
| Also, frankly, its worth as a programming technique is controversial: the [Fragile Base Class Problem](https://en.wikipedia.org/wiki/Fragile_base_class) can cause significant harm to a project. | ||
|
|
||
| Lastly, Luau easily supports interface inheritance through its structural type system, so inheritance is judged to be lower priority. |
There was a problem hiding this comment.
Luau does not support interface inheritance through the structural type system for any type that has methods because method types are not expressible in the type system. Examples:
type interface = { foo: (self: interface) -> () }
type something = { value: number, foo: (self: something) -> () }
local function fn(obj: interface)
obj:foo()
end
local function create(): something
return {
value = 0,
foo = function(self: something)
print(self.value)
end,
}
end
fn(create()) -- type error!type interface = { foo: <self>(self: self) -> () }
type something = { value: number, foo: (self: something) -> () }
local function fn(obj: interface)
obj:foo()
end
local function create(): something
return {
value = 42,
foo = function(self: something)
print(self.value)
end,
}
end
fn(create()) -- type error!type interface = { foo: <self>(self: self) -> () }
local function fn(obj1: interface, obj2: interface)
obj1.foo(obj2) -- runtime error!
endThere are other avenues, but I believe I have explored them all, and it seems that there is no way to express interfaces with methods in Luau in a way where the type system gives no false positives and where it also catches common runtime errors. And this is without getting into generic interfaces.
There was a problem hiding this comment.
You're right. Our current structural type system can't capture this.
At first, I thought this could be resolved via read-only table properties, but that's not sufficient:
type interface = { read myMethod: (interface) -> () }
type impl = { value: number, read myMethod: (impl) -> () }
function accept(i: interface) i:myMethod() end
local o: impl = nil :: any
accept(o)In this code fragment, the call to accept requires that impl <: interface, but that subtyping test requires that (impl) -> () <: (interface) -> (). Function argument types are tested contravariantly, so we also require that interface <: impl and we fail.
I think the way we solve this is by adding a special self type to the language that would allow the above fragment to typecheck. I'll tee up a separate RFC to tackle this.
|
|
||
| #### Class Objects | ||
|
|
||
| The action of evaluating a class definition statement introduces a *class object* in the module scope. A class object is a value that serves as a factory for instances of the class and as a namespace for any functions that are defined on the class. |
There was a problem hiding this comment.
Are class objects values? Can they be passed around? What is their type? What happens if you pass a class object into instanceof or type?
There was a problem hiding this comment.
Class objects are values that mostly behave the same as class instances.
They are not considered to be members of any class type, so instanceof will always return false.
This could change later if we wanted to introduce a type hierarchy a la Python.
There was a problem hiding this comment.
Should instanceof(MyClass, class) return true (assuming we introduced a class lib)
|
Looks pretty great, just some details that need to be ironed out in the RFC process! |
|
I'm on board insofar as polishing OOP in Luau, but some of the specifics of this specific proposal leave me feeling a bit iffy. The initial lack of support for private fields and methods is disappointing — understandable from the perspective of how the RFC describes the complications, but disappointing nonetheless. My concern with its omission pertains largely (if not entirely) to syntax and readability. Assuming users do not subscribe to prefixing bindings with There is "tension" between the syntax for fields and methods that we could honestly do better about. If functions are public by default, why do fields need to be declared with access modifiers? I'm also of the opinion that |
|
I don't think Even if a class Cat
field name: string
private field internal_id: number
function meow(self, text: string)
end
endIn my opinion, |
|
I find the rationale on implementing visibility modifiers being in the vein of "may as well because we have to introduce some keyword so it doesn't look weird" to be very weak. This just seems like a very bad way to decide on language features. Back on visibility modifiers, I think that if the goal is to avoid unwanted access, then there are better ways that already exist at the API or representation by putting that state behind handles, whether they are opaque references or simple indices into other structures. There does not need to be some keyword that adds a whole charade to how that data is being accessed that can ultimately be bypassed ostensibly. |
Roblox has, in the past, released APIs written in Luau using conventions like Something more robust is needed. I'm working on a separate RFC to get into the details of this.
We think there's a performance win to be had here: Class instances don't need the array part of a table, and we think that we can save some memory and improve cache locality by splitting the property hash table. The class object holds the set of keys in a particular class. The class instances just hold an array of values.
Shared-self unfortunately did not work out. I spent a couple of months trying to implement it, and it turned out to be a lot more brittle than I had hoped. I cover this briefly in the RFC. |
I still don't think this issue necessitates the implementation of access specifiers. Like I mentioned, an upvalue system could just be preferred instead. class A
function new()
local privateField = 0
local object = A()
object.getter = function()
return privateField
end
return object
end
endI believe a solution like this would be a better workaround. Of course, if private fields could be optimized somehow by the compiler, many people would be more okay with it. That aside, I do believe the reasoning as to why public and private exists is weak. No keyword looks great, and if we're still going to have keywords for fields, then it should be made with a keyword like "field".
That really sucks. Custom class modules perhaps would have benefitted from it. However, native classes in Luau sounds and works as a better option. I do look forward to potential optimizations, describing some of them would give this RFC a better foundation to work with. |
This restriction could be lifted someday. It's in place now just to keep things simple. |
This is probably okay to add at a later date. It requires some extra complexity in the VM, but it should be fine. |
|
The only reason I'm adamant about the parentheses is because unlike conventional OO languages, the fields of Luau classes are the constructor and have to be initialized. The standard syntax for fields doesn't reflect that since most languages like C# or JavaScript just treat their fields as zero-initialized/undefined until the constructor assigns a new value to them. Luau classes don't have the same mechanics, so the syntax is just a bad fit. Mirroring a function call with the prior art of "default constructors" makes it very clear that fields are also the constructor and have to be initialized with something. It also neatly separates the instance fields from the class body, which opens up the possibility of adding other constructs like static fields to the class body easily: class Person(name: string, id: number)
local currentId = 0 -- static field
function new(name: string)
currentId += 1
return Person(name, currentId)
end
endI'd like to see how static fields could be done in a cleaner way than this. |
|
@alexmccord Also, just saying that making the constructor a normal function call solves your issue with missing keys since function arity isn't affected by nil: local x = Name("Foo", "Bar", "Baz") -- valid, 3 args
local y = Name("John", nil, "Doe") -- valid, 3 args
local z = Name("Jane", "Doe") -- invalid, 2 args |
|
I like the idea behind having fields "like function parameters" but do not like ordered parameters for construction. I have found these to be quite fragile when working with classes with a large number of fields, and always resulted in me creating a "from_table" static method. It would be interesting syntax, possibly to extend to functions, where they can be defined using If that sounds like a useful extension then we could at first use Mostly just food for thought. function foo{ name: string, reps: number | nil }
for i = 1, reps or 1 do
print("Hi " .. name)
end
end
class Cat{ name: string }
function speak(self)
print(self.name)
end
end
foo{ name = "Alice", reps = 5 }
local bob = Cat{ name = "Bob" } |
|
I feel like () this should be the case class Cat( name: string ) function foo( name: string, reps: number | nil ) And luau do a python with named variables foo( name = "Alice", reps = 5 ) |
|
Could this RFC be merged because its beeb 2 weeks with all of the major issues resolved? As I'd like to include class properties in #147 |
|
Yeah. I think this is just about ready to go. Unless something surprising happens, I'll merge this RFC on Monday the 27th. Thanks so much for the detailed feedback everyone! |
function __tostring(self)
return `Point \{ x = {self.x}, y = {self.y} \}`
endWhat if I don't want to type Like, I am looking at the above, and there's not even colors there. And if I'd copy paste this to someone, would they even know that this is for a class, if I didn't even include the portion there I wish there was some keyword function class:__tostring()
return `Point \{ x = {self.x}, y = {self.y} \}`
endSo, I could just do this. This doesn't mean the class is named |
|
I cannot support merging if hoisting the class identifier is still planned. I don't care if everything else is perfect, variable hoisting is an unconditionally terrible feature that must be avoided at all costs. Many dynamic languages like Python and Wren don't even bother with solving the mutually dependent class problem, and the ones that do like JavaScript solve it by invoking variable hoisting, where TDZ ensues. One of the best things about Lua(u) was that it did local variables right and only made identifiers visible in the order they lexically appeared, because any other visibility rule would be utterly confusing. Making classes an exception ruins this aspect of the language. Again, you can still pull this off by using an explicit forward-declared variable. It's ugly, but it's the least evil option here. local _B
class A
function f() return _B {} end
end
class B
function g() return A {} end
end
_B = B |
|
An alternative to hoisting is some kind of syntax for mutually dependent declarations. You already need this with mutually dependent functions (whether class Foo
function f() return Bar.f() end
end
and Bar
function f() return Foo.f() end
end |
|
Python solves this problem essentially by hoisting everything. def getX():
return x
x = 22
print(getX()) |
|
How would this class look like https://paste.ivr.fi/irylyfegux.lua Since it seems to use If only proxies would solve it, then it would be a bit disappointing maybe. Unless there's |
| Fields are introduced with the new `public` keyword. We also plan to eventually offer `private`, but is sufficiently complex that it merits its own RFC. | ||
|
|
||
| Methods are introduced with the familiar `function` keyword. `public function f()` is also permitted. | ||
|
|
||
| Methods defined on class objects can be accessed either via `Class.method()` or `instance:method()` syntax. | ||
|
|
||
| If a method's first argument is named `self`, it should be invoked with the familiar `instance:method()` call syntax. This is not strictly required, but the compiler and optimizers may deoptimize code that doesn't. Type annotations on the `self` parameter are not allowed. | ||
|
|
||
| If a method accepts no arguments or if its first argument is not named `self`, it should be invoked via `Class.method()` syntax. This is the same as "static methods" from other languages. |
There was a problem hiding this comment.
I haven't seen any explicit mentions of whether or not fields can be defined as functions. I can therefore only assume by the fact there isn't a good enough distinction between methods and fields, that defining fields as functions would currently be impossible—at least from outside constructor functions...
...but what about inside constructors? You can define callbacks in JavaScript classes like so:
class Foo {
constructor(bar) {
this.bar = bar;
// Callback attribute:
this.onJarp = () => {
console.log(`${this.bar} yarb!`);
};
}
jarp() {
if (typeof this.onClick === 'function') {
// Invoke the callback:
this.onJarp();
}
}
}
// Usage example
const myFoo = new Foo('Bar');
// Custom callback assigned later
myFoo.onJarp = () => {
console.log('This is a custom callback!');
};
// Trigger the callback
myButton.jarp();Just wondering whether or not Luau could support this type of functionality. I'd prefer not to be forced to define callbacks through a method, as the use of fields and methods should convey intention about what exactly is changing about a class's instance; fields are used to house data that can be used either internally, or externally; methods are the actions that change, or are shown to "do something" to the instance. Setting a callback through a method isn't really "doing something" to the object, which is why I am not really a fan of that approach.
There was a problem hiding this comment.
Fields aren't immutable as far as I understand so the following should work, especially considering classes are meant to be a replacement for metatable classes.
class Foo
public onJarp: ((foo: Foo) -> ())?
function create(bar: string)
local function onJarp(foo: Foo)
print(`{foo.bar} yarb!`)
end
return Foo { bar = bar, onJarp = onJarp }
end
function Jarp(self)
if self.onJarp then
self.onJarp(self)
end
end
end
const myFoo = Foo.create("Bar")
myFoo.onJarp = function(Foo)
print("This is a custom callback!")
end
myFoo:Jarp()There was a problem hiding this comment.
The only awkward area I can still see is that there is no way to initialize functions as attributes without accidentally creating methods outside of constructors. It's annoying for one as it means that you may end up having to define the same logic multiple times throughout differing constructors (if you're going to use "static methods") that would otherwise be included in class definitions statements instead.
There was a problem hiding this comment.
I wonder how or how complex it will be for code editors to jump to the implementation of onJarp
|
|
||
| Defining two classes with the same name in the same module is forbidden. | ||
|
|
||
| Within a class block, two declarations are allowed: Fields and methods. |
There was a problem hiding this comment.
What would happen if one makes a field, such as .data, which could be anything.
in the old method, you'd probably have self.data = {} and as you fill it in, in the "class.new" constructor, you'd mutate the entire object. Though I think Luau would be able to know and infer data and its mutation into object correctly?
And what about initializing functions? Some would initialize things, mid running .new()
function new(x, y)
local newPoint = Point { x = x, y = y }
newPoint:PrepareThings()
return newPoint
endas in
function Point.new(x, y)
local newPoint = setmetatable({ x = x, y = y }, Point)
newPoint:PrepareThings()
return newPoint
end
And what would happen in this case here?
function new(x, y)
local newPoint = Point { data = {} }
newPoint.data.entry = "test"
return newPoint
endWould it know about .data having entry in it? Because this is the dynamic way, on how you'd do it in the previous way. Along with any kind of table "composing" functions as well.
| * `__idiv` | ||
|
|
||
| For forward-compatibility, it is a syntax error to define any other method whose | ||
| name starts with two underscores. |
There was a problem hiding this comment.
I don't see __index and or __newindex on there.
Now, I am not suggesting that if someone would re-define __index in a class, that it's like 1:1 same behavior as in a table. I am more thinking of sugar.
But I was looking for what types of classes exist, and found this special one. It seems to have self.data, it's some kind of array. It's like as if someone made something like Dictionary in C#, you'd be able to index into the object as in like dict["something"], but at the same time, you're also able to use methods dict.TryGetValue(). But since __newindex or __index isn't mentioned, you kinda can't do that?
Unless you'd make a proxy, but a proxy, is just another hack, for these 2 things. Proxies maybe are useful for other debugging. But a proxy for these two things, doesn't feel like it.
Details
--[[
A general purpose n-dimensional vector library. mul and div support either scalar multiplication or componentwise with another vector
]]
local FloatVector = {}
type FloatVectorMembers = { data: { number } }
export type FloatVector = typeof(setmetatable({} :: FloatVectorMembers, FloatVector))
-- input: Another FloatVector or tuple of number values
function FloatVector.new(...): FloatVector
local new = setmetatable({} :: FloatVectorMembers, FloatVector)
new.data = {}
local inputTable = { ... } :: any
if #inputTable == 1 then
if typeof(inputTable[1]) == "table" then
if getmetatable(inputTable[1]) :: any == FloatVector then
inputTable = (inputTable[1] :: FloatVector).data
else
inputTable = inputTable[1] :: { number }
end
end
end
for _, component in inputTable do
if typeof(component) ~= "number" then
error("arg to FloatVector.new() not a number")
else
table.insert(new.data, component :: number)
end
end
return new
end
function FloatVector:getSize(): number
return #self.data
end
function FloatVector.checkCompatible(lhs: FloatVector, rhs: FloatVector): boolean
if typeof(lhs) ~= typeof(FloatVector) or typeof(rhs) ~= typeof(FloatVector) then
error("vector operation on non-vector type")
return false
end
if #lhs.data ~= #rhs.data then
error("operating on vectors of different dimension")
return false
end
return true
end
function FloatVector.__index(table, key: any)
if typeof(key) == "number" then
return table.data[key :: number]
end
return FloatVector[key]
end
function FloatVector.__newindex(table, key, value)
if typeof(key) == "number" then
table.data[key :: number] = value :: number
return
end
rawset(table, key, value)
end
function FloatVector.__add(lhs: FloatVector, rhs: FloatVector): FloatVector
FloatVector.checkCompatible(lhs, rhs)
local result = {}
for i = 1, #lhs.data, 1 do
table.insert(result, lhs.data[i] + rhs.data[i])
end
return FloatVector.new(result)
end
function FloatVector.__sub(lhs: FloatVector, rhs: FloatVector): FloatVector
FloatVector.checkCompatible(lhs, rhs)
local result = {}
for i = 1, #lhs.data, 1 do
table.insert(result, lhs.data[i] - rhs.data[i])
end
return FloatVector.new(result)
end
function FloatVector.__mul(lhs: FloatVector, rhs: FloatVector | number): FloatVector
if typeof(lhs) == typeof(FloatVector) and typeof(rhs) == "number" then
local result = {}
for _, component in lhs.data do
table.insert(result, component * rhs :: number)
end
return FloatVector.new(result)
else
FloatVector.checkCompatible(lhs, rhs)
local result = {}
for i = 1, #lhs.data, 1 do
table.insert(result, lhs.data[i] * rhs.data[i])
end
return FloatVector.new(result)
end
end
function FloatVector.__div(lhs: FloatVector, rhs: FloatVector | number): FloatVector
if typeof(lhs) == typeof(FloatVector) and typeof(rhs) == "number" then
local result = {}
for _, component in lhs.data do
table.insert(result, component / rhs :: number)
end
return FloatVector.new(result)
else
FloatVector.checkCompatible(lhs, rhs)
local result = {}
for i = 1, #lhs.data, 1 do
table.insert(result, lhs.data[i] / rhs.data[i])
end
return FloatVector.new(result)
end
end
function FloatVector:dot(rhs): number
FloatVector.checkCompatible(self, rhs)
local result = 0
for i = 1, #self.data, 1 do
result += self.data[i] * rhs.data[i]
end
return result
end
-- must be a 3 dimensional vector
function FloatVector:cross(rhs): FloatVector?
FloatVector.checkCompatible(self, rhs)
if #self.data ~= 3 or #rhs.data ~= 3 then
error("cross product is only defined for two 3 dimensional vectors")
return nil
end
return FloatVector.new(
self.data[2] * rhs.data[3] - self.data[3] * rhs.data[2],
self.data[3] * rhs.data[1] - self.data[1] * rhs.data[3],
self.data[1] * rhs.data[2] - self.data[2] * rhs.data[1]
)
end
function FloatVector:magnitude(): number
local sqSum = 0
for _, component in self.data do
sqSum += component ^ 2
end
return math.sqrt(sqSum)
end
function FloatVector:fuzzyEq(rhs: FloatVector, eps: number): boolean
if not FloatVector.checkCompatible(self, rhs) then
return false
end
eps = if eps then eps else 0.00001
for i = 1, #self.data, 1 do
if math.abs(self.data[i] - rhs.data[i]) > eps then
return false
end
end
return true
end
return FloatVectorThere was a problem hiding this comment.
But since __newindex or __index isn't mentioned, you kinda can't do that?](#191)
It may be coming in the future as mentioned here: #191 (comment)
|
What would happen if someone passed a class through networking? Would there be a
With table fake classes, since it's already a table, it could just pass that through, right? But how fast would a class to table conversion be and etc.? If an object has just |
|
Forgive me if this has already been mentioned, but will you be able to monkey patch a function or a variable onto an object? |
|
I would really like for a second thought wrt whether to use the local keyword or not for classes. I had previously made my case above, and would love to know as to whether there is a stronger reason for going against it. |
You can change a variable(as soon as it is public) but not a function(method). |
|
I also would like some reconsideration on my parentheses proposal. Like I've said, the current "spreadsheet" style for fields is unideal because that syntax is only good for languages that don't make an effort to generate a default constructor from the fields. Luau does, so if this syntax stays it will become extremely awkward to add things like static fields later down the line when they sensibly shouldn't be a part of the constructor. My proposal cleanly separates constructor from class implementation, making this distinction trivial. |
|
@MagmaBurnsV I don't think there's any issues there. You can think of class Point
public x: number
public y: number
public function components(self)
return self.x, self.y
end
endThis is equivalent to the following: class Point
public x: number
public y: number
public static const components = function(self: Point)
return self.x, self.y
end
end(with the difference that this |
|
I was talking about static fields being included in the constructor itself. Take this example: class Person
public name: string
private static nextId = 0
public id: number
endDoes the constructor include Compare this with my version: class Person(name: string, id: number)
local nextId = 0
endNow it's clear only |
|
Shouldn't classes have a default |
Python does actually have a "solution" for this in its types, and it's so truly horrendous that I think it argues strongly in favour of at least some degree of hoisting. If a class needs to reference a class defined below itself, the type can be written as |
Small distinction, __tostring metamethod is not required for values to have a string representation. |
Rendered.